package org.adw.library.widgets.discreteseekbar;

import ohos.agp.components.*;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.utils.Color;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.multimodalinput.event.TouchEvent;
import org.adw.library.widgets.discreteseekbar.internal.AttrUtils;
import org.adw.library.widgets.discreteseekbar.internal.ColorStateList;
import org.adw.library.widgets.discreteseekbar.internal.PopupIndicator;
import org.adw.library.widgets.discreteseekbar.internal.drawable.AlmostRippleDrawable;
import org.adw.library.widgets.discreteseekbar.internal.drawable.ThumbDrawable;
import org.adw.library.widgets.discreteseekbar.internal.drawable.TrackRectDrawable;

import java.util.Formatter;
import java.util.Locale;

public class DiscreteSeekBar extends Component implements Component.DrawTask, Component.TouchEventListener {
    public static int TOP_BAR_HEIGHT = 129;

    private static final String DEFAULT_FORMATTER = "%d";

    private static final int PROGRESS_ANIMATION_DURATION = 250;
    private static final int INDICATOR_DELAY_FOR_TAPS = 150;
    private static final int DEFAULT_THUMB_COLOR = 0xff009688;
    private static final int SEPARATION_DP = 5;

    private AlmostRippleDrawable mRipple;
    private ThumbDrawable mThumb;
    private TrackRectDrawable mTrack;
    private TrackRectDrawable mScrubber;


    private int mTrackHeight;
    private int mScrubberHeight;
    private int mAddedTouchBounds;

    private int mMax = 100;
    private int mMin = 0;
    private int mValue = 0;
    private int mKeyProgressIncrement = 1;
    private boolean mAllowTrackClick = true;
    private boolean mIndicatorPopupEnabled = true;

    // We use our own Formatter to avoid creating new instances on every progress change

    Formatter mFormatter;
    private String mIndicatorFormatter;
    private NumericTransformer mNumericTransformer;
    private StringBuilder mFormatBuilder;
    private OnProgressChangeListener mPublicChangeListener;
    private boolean mIsDragging;
    private int mDragOffset;

    private PopupIndicator mIndicator;

    private int indicatorTextSize;
    private int thumbSize;
    private int separation;

    public DiscreteSeekBar(Context context) {
        this(context, null, null);
    }

    public DiscreteSeekBar(Context context, AttrSet attrSet) {
        this(context, attrSet, null);
    }

    public DiscreteSeekBar(Context context, AttrSet attrSet, String styleName) {
        super(context, attrSet, styleName);
        init(attrSet);
    }

    private void init(AttrSet attrSet) {
        TOP_BAR_HEIGHT = AttrUtils.getIntFromAttr(attrSet, "dsb_topBarHeight", dp2px(43));
        mAllowTrackClick = AttrUtils.getBooleanFromAttr(attrSet, "dsb_allowTrackClickToDrag", mAllowTrackClick);
        mIndicatorPopupEnabled = AttrUtils.getBooleanFromAttr(attrSet, "dsb_indicatorPopupEnabled", mIndicatorPopupEnabled);
        mTrackHeight = AttrUtils.getDimensionFromAttr(attrSet, "dsb_trackHeight", dp2px(1));
        mScrubberHeight = AttrUtils.getDimensionFromAttr(attrSet, "dsb_scrubberHeight", dp2px(4));
        indicatorTextSize = AttrUtils.getDimensionFromAttr(attrSet, "dsb_indicatorTextSize", dp2px(12));
        thumbSize = AttrUtils.getDimensionFromAttr(attrSet, "dsb_thumbSize", dp2px(ThumbDrawable.DEFAULT_SIZE_DP));
        separation = AttrUtils.getDimensionFromAttr(attrSet, "dsb_indicatorSeparation", dp2px(SEPARATION_DP));
        int touchBounds = dp2px(32);
        mAddedTouchBounds = Math.max(touchBounds, thumbSize) / 2;
        mMax = AttrUtils.getIntFromAttr(attrSet, "dsb_max", mMax);
        mMin = AttrUtils.getIntFromAttr(attrSet, "dsb_min", mMin);
        mValue = AttrUtils.getIntFromAttr(attrSet, "dsb_value", mValue);
        if (mMax <= mMin) {
            mMax = mMin + 1;
        }
        if (mValue > mMax || mValue < mMin) {
            mValue = mMin;
        }
        mIndicatorFormatter = AttrUtils.getStringFromAttr(attrSet, "dsb_indicatorFormatter", null);

        initDrawable(attrSet);
        setTouchEventListener(this);
        addDrawTask(this);
        setLayoutRefreshedListener(component -> invalidate());
    }

    private void initDrawable(AttrSet attrSet) {
        int trackNormalColor = AttrUtils.getColorFromAttr(attrSet, "dsb_trackColor", Color.GRAY.getValue());
        int trackPressedColor = AttrUtils.getColorFromAttr(attrSet, "dsb_trackPressedColor", trackNormalColor);
        int trackFocusedColor = AttrUtils.getColorFromAttr(attrSet, "dsb_trackFocusedColor", trackNormalColor);
        int trackDisabledColor = AttrUtils.getColorFromAttr(attrSet, "dsb_trackDisabledColor", trackNormalColor);
        ColorStateList trackColor = new ColorStateList();
        trackColor.addState(new int[]{ComponentState.COMPONENT_STATE_EMPTY}, trackNormalColor);
        trackColor.addState(new int[]{ComponentState.COMPONENT_STATE_PRESSED}, trackPressedColor);
        trackColor.addState(new int[]{ComponentState.COMPONENT_STATE_FOCUSED}, trackFocusedColor);
        trackColor.addState(new int[]{ComponentState.COMPONENT_STATE_DISABLED}, trackDisabledColor);
        mTrack = new TrackRectDrawable(trackColor, this);

        int progressNormalColor = AttrUtils.getColorFromAttr(attrSet, "dsb_progressColor", DEFAULT_THUMB_COLOR);
        int progressPressedColor = AttrUtils.getColorFromAttr(attrSet, "dsb_progressPressedColor", progressNormalColor);
        int progressFocusedColor = AttrUtils.getColorFromAttr(attrSet, "dsb_progressFocusedColor", progressNormalColor);
        int progressDisabledColor = AttrUtils.getColorFromAttr(attrSet, "dsb_progressDisabledColor", progressNormalColor);
        ColorStateList progressColor = new ColorStateList();
        progressColor.addState(new int[]{ComponentState.COMPONENT_STATE_EMPTY}, progressNormalColor);
        progressColor.addState(new int[]{ComponentState.COMPONENT_STATE_PRESSED}, progressPressedColor);
        progressColor.addState(new int[]{ComponentState.COMPONENT_STATE_FOCUSED}, progressFocusedColor);
        progressColor.addState(new int[]{ComponentState.COMPONENT_STATE_DISABLED}, progressDisabledColor);
        mScrubber = new TrackRectDrawable(progressColor, this);
        mThumb = new ThumbDrawable(progressColor, this, thumbSize);

        int rippleNormalColor = AttrUtils.getColorFromAttr(attrSet, "dsb_rippleColor", Color.DKGRAY.getValue());
        int ripplePressedColor = AttrUtils.getColorFromAttr(attrSet, "dsb_ripplePressedColor", rippleNormalColor);
        int rippleFocusedColor = AttrUtils.getColorFromAttr(attrSet, "dsb_rippleFocusedColor", rippleNormalColor);
        int rippleDisabledColor = AttrUtils.getColorFromAttr(attrSet, "dsb_rippleDisabledColor", rippleNormalColor);
        ColorStateList rippleColor = new ColorStateList();
        rippleColor.addState(new int[]{ComponentState.COMPONENT_STATE_EMPTY}, rippleNormalColor);
        rippleColor.addState(new int[]{ComponentState.COMPONENT_STATE_PRESSED}, ripplePressedColor);
        rippleColor.addState(new int[]{ComponentState.COMPONENT_STATE_FOCUSED}, rippleFocusedColor);
        rippleColor.addState(new int[]{ComponentState.COMPONENT_STATE_DISABLED}, rippleDisabledColor);
        mRipple = new AlmostRippleDrawable(rippleColor, this);
        setNumericTransformer(new DefaultNumericTransformer());
        mIndicator = new PopupIndicator(this, attrSet, calculateMarkerWidth(), thumbSize, separation);
    }

    @Override
    public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
        if (touchEvent.getAction() == TouchEvent.PRIMARY_POINT_DOWN && touchEvent.getPointerCount() == 1) {
            int index = getIndexFromX(getTouchX(touchEvent, 0));
            if (mAllowTrackClick || Math.abs(getOffset(index) - getOffset(mValue - mMin)) < dp2px(20)) {
                mIsDragging = true;
                if (mPublicChangeListener != null) {
                    mPublicChangeListener.onStartTrackingTouch(DiscreteSeekBar.this);
                }
                if (mValue != index + mMin) {
                    mValue = index + mMin;
                    notifyProgress(mValue, true);
                }
                mRipple.animateToPressed();
                if (mIndicatorPopupEnabled) {
                    mIndicator.animateToPressed();
                    if (mNumericTransformer.useStringTransform()) {
                        mIndicator.refresh(getDialogX(), getDialogY(), mNumericTransformer.transformToString(mValue));
                    } else {
                        mIndicator.refresh(getDialogX(), getDialogY(), convertValueToMessage(mNumericTransformer.transform(mValue)));
                    }
                }
                disableScroll();
                invalidate();
                return true;
            }
        } else if (touchEvent.getAction() == TouchEvent.POINT_MOVE) {
            if (mIsDragging) {
                int index = getIndexFromX(getTouchX(touchEvent, 0));
                if (mValue != index + mMin) {
                    mValue = index + mMin;
                    notifyProgress(mValue, true);
                }
                if (mNumericTransformer.useStringTransform()) {
                    mIndicator.refresh(getDialogX(), getDialogY(), mNumericTransformer.transformToString(mValue));
                } else {
                    mIndicator.refresh(getDialogX(), getDialogY(), convertValueToMessage(mNumericTransformer.transform(mValue)));
                }
                invalidate();
                return true;
            }
        } else if (touchEvent.getAction() == TouchEvent.PRIMARY_POINT_UP && touchEvent.getPointerCount() == 1 || touchEvent.getAction() == TouchEvent.CANCEL) {
            if (mIsDragging || mIndicator != null && mIndicator.isOpen()) {
                mRipple.animateToNormal();
                if (mIndicator != null) {
                    mIndicator.animateToNormal();
                }
                if (mPublicChangeListener != null) {
                    mPublicChangeListener.onStopTrackingTouch(DiscreteSeekBar.this);
                }
                enableScroll();
                invalidate();
            }
            mIsDragging = false;
        }
        return true;
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        int width = getWidth();
        int height = getHeight();
        ComponentContainer.LayoutConfig layoutConfig = getLayoutConfig();
        if (layoutConfig.height == ComponentContainer.LayoutConfig.MATCH_CONTENT) {
            layoutConfig.height = Math.max(thumbSize, dp2px(32));
            height = layoutConfig.height;
            setLayoutConfig(layoutConfig);
        }
        mDragOffset = getOffset(mValue - mMin);
        mRipple.doDraw(canvas, mDragOffset, height / 2, dp2px(16));
        mTrack.doDraw(canvas, new RectFloat(mAddedTouchBounds,
                (height - mTrackHeight) / 2.0f,
                width - mAddedTouchBounds,
                (height + mTrackHeight) / 2.0f));
        mScrubber.doDraw(canvas, new RectFloat(mAddedTouchBounds,
                (height - mScrubberHeight) / 2.0f,
                mDragOffset,
                (height + mScrubberHeight) / 2.0f));
        if (!mIndicatorPopupEnabled || mIndicator == null || !mIndicator.isOpen()) {
            mThumb.doDraw(canvas, mDragOffset, height / 2);
        }
    }

    private float calculateMarkerWidth() {
        float length = 0;
        for (int i = mMin; i <= mMax; i++) {
            if (mNumericTransformer.useStringTransform()) {
                Paint paint = new Paint();
                paint.setTextSize(indicatorTextSize);
                float textLength = paint.measureText(mNumericTransformer.transformToString(i));
                if (textLength > length) {
                    length = textLength;
                }
            } else {
                Paint paint = new Paint();
                paint.setTextSize(indicatorTextSize);
                float textLength = paint.measureText(convertValueToMessage(mNumericTransformer.transform(i)));
                if (textLength > length) {
                    length = textLength;
                }
            }
        }
        return length;
    }

    private float getDialogX() {
        int offsetX = getOffset(mValue - mMin);
        int[] xy = getLocationOnScreen();
        if (xy != null && xy.length == 2) {
            return offsetX + xy[0];
        }
        return offsetX;
    }

    private float getDialogY() {
        int[] xy = getLocationOnScreen();
        if (xy != null && xy.length == 2) {
            return getHeight() / 2 + xy[1];
        }
        return getHeight() / 2;
    }

    private float getTouchX(TouchEvent touchEvent, int index) {
        float touchX = 0;
        if (touchEvent.getPointerCount() > index) {
            int[] xy = getLocationOnScreen();
            if (xy != null && xy.length == 2) {
                touchX = touchEvent.getPointerScreenPosition(index).getX() - xy[0];
            } else {
                touchX = touchEvent.getPointerPosition(index).getX();
            }
        }
        return touchX;
    }

    private float getTouchY(TouchEvent touchEvent, int index) {
        float touchY = 0;
        if (touchEvent.getPointerCount() > index) {
            int[] xy = getLocationOnScreen();
            if (xy != null && xy.length == 2) {
                touchY = touchEvent.getPointerScreenPosition(index).getY() - xy[1];
            } else {
                touchY = touchEvent.getPointerPosition(index).getY();
            }
        }
        return touchY;
    }

    private int getIndexFromX(float x) {
        if (x <= mAddedTouchBounds) {
            return 0;
        } else if (x >= getWidth() - mAddedTouchBounds) {
            return mMax - mMin;
        }

        float offsetX = x - mAddedTouchBounds;
        int all = mMax - mMin;
        float oneLength = (getWidth() - 2 * mAddedTouchBounds) * 1.0f / all;
        int left = (int) (offsetX / oneLength);
        if (offsetX - left > oneLength / 2) {
            return left + 1;
        } else {
            return left;
        }
    }

    private int getOffset(int progressIndex) {
        int all = mMax - mMin;
        float oneLength = (getWidth() - 2 * mAddedTouchBounds) * 1.0f / all;
        return (int) (progressIndex * oneLength + mAddedTouchBounds);
    }

    private void updateProgressMessage(int value) {
        if (mIndicator != null) {
            mIndicator.setMarkLength(calculateMarkerWidth());
            if (mNumericTransformer.useStringTransform()) {
                mIndicator.refresh(getDialogX(), getDialogY(), mNumericTransformer.transformToString(value));
            } else {
                mIndicator.refresh(getDialogX(), getDialogY(), convertValueToMessage(mNumericTransformer.transform(value)));
            }
        }
    }

    private String convertValueToMessage(int value) {
        String format = mIndicatorFormatter != null ? mIndicatorFormatter : DEFAULT_FORMATTER;
        if (mFormatter == null || !mFormatter.locale().equals(Locale.getDefault())) {
            int bufferSize = format.length() + String.valueOf(mMax).length();
            if (mFormatBuilder == null) {
                mFormatBuilder = new StringBuilder(bufferSize);
            } else {
                mFormatBuilder.ensureCapacity(bufferSize);
            }
            mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
        } else {
            mFormatBuilder.setLength(0);
        }
        return mFormatter.format(format, value).toString();
    }


    public void setIndicatorFormatter(String formatter) {
        mIndicatorFormatter = formatter;
        updateProgressMessage(mValue);
    }

    public void setNumericTransformer(NumericTransformer transformer) {
        mNumericTransformer = transformer != null ? transformer : new DefaultNumericTransformer();
        updateProgressMessage(mValue);
    }

    public NumericTransformer getNumericTransformer() {
        return mNumericTransformer;
    }

    public void setMax(int max) {
        mMax = max;
        if (mMax < mMin) {
            setMin(mMax - 1);
        }
        if (mValue < mMin || mValue > mMax) {
            setProgress(mMin);
        }
        invalidate();
    }

    public int getMax() {
        return mMax;
    }

    public void setMin(int min) {
        mMin = min;
        if (mMin > mMax) {
            setMax(mMin + 1);
        }
        if (mValue < mMin || mValue > mMax) {
            setProgress(mMin);
        }
        invalidate();
    }

    public int getMin() {
        return mMin;
    }

    public void setProgress(int progress) {
        setProgress(progress, false);
    }

    private void setProgress(int value, boolean fromUser) {
        value = Math.max(mMin, Math.min(mMax, value));
        if (mValue != value) {
            mValue = value;
            notifyProgress(value, fromUser);
            updateProgressMessage(value);
        }
        invalidate();
    }

    private void notifyProgress(int value, boolean fromUser) {
        if (mPublicChangeListener != null) {
            mPublicChangeListener.onProgressChanged(DiscreteSeekBar.this, value, fromUser);
        }
    }


    public int getProgress() {
        return mValue;
    }

    public void setOnProgressChangeListener(OnProgressChangeListener listener) {
        mPublicChangeListener = listener;
    }

    public void setThumbColor(int thumbColor, int indicatorColor, int indicatorPressedColor) {
        mThumb.setColor(thumbColor);
        mIndicator.setColors(indicatorColor, indicatorPressedColor);
        invalidate();
    }

    public void setThumbColor(ColorStateList thumbColorStateList, int indicatorColor, int indicatorPressedColor) {
        mThumb.setColorStateList(thumbColorStateList);
        mIndicator.setColors(indicatorColor, indicatorPressedColor);
        invalidate();
    }

    public void setScrubberColor(int color) {
        mScrubber.setColor(color);
        invalidate();
    }

    public void setScrubberColor(ColorStateList colorStateList) {
        mScrubber.setColorStateList(colorStateList);
        invalidate();
    }

    public void setRippleColor(int color) {
        mRipple.setColor(color);
        invalidate();
    }

    public void setRippleColor(ColorStateList colorStateList) {
        mRipple.setColorStateList(colorStateList);
        invalidate();
    }

    public void setTrackColor(int color) {
        mTrack.setColor(color);
        invalidate();
    }

    public void setTrackColor(ColorStateList colorStateList) {
        mTrack.setColorStateList(colorStateList);
        invalidate();
    }

    public void setIndicatorPopupEnabled(boolean enabled) {
        this.mIndicatorPopupEnabled = enabled;
        invalidate();
    }

    public interface OnProgressChangeListener {
        public void onProgressChanged(DiscreteSeekBar seekBar, int value, boolean fromUser);

        public void onStartTrackingTouch(DiscreteSeekBar seekBar);

        public void onStopTrackingTouch(DiscreteSeekBar seekBar);
    }

    public static abstract class NumericTransformer {
        public abstract int transform(int value);

        public String transformToString(int value) {
            return String.valueOf(value);
        }

        public boolean useStringTransform() {
            return false;
        }
    }


    private static class DefaultNumericTransformer extends NumericTransformer {
        @Override
        public int transform(int value) {
            return value;
        }
    }

    private int dp2px(float dp) {
        return (int) (getResourceManager().getDeviceCapability().screenDensity / 160 * dp);
    }

    private void disableScroll() {
        final ComponentContainer parent = (ComponentContainer) getComponentParent();
        Component component = findParentScroll(parent);
        if (component != null) {
            component.setEnabled(false);
        }
    }

    private void enableScroll() {
        final ComponentContainer parent = (ComponentContainer) getComponentParent();
        Component component = findParentScroll(parent);
        if (component != null) {
            component.setEnabled(true);
        }
    }

    private Component findParentScroll(ComponentContainer view) {
        if (view instanceof ScrollView) {
            return view;
        } else if (view instanceof NestedScrollView) {
            return view;
        } else if (view instanceof ListContainer) {
            return view;
        }
        final ComponentParent parent = view.getComponentParent();
        if (parent instanceof ComponentContainer) {
            return findParentScroll((ComponentContainer) parent);
        } else { //root view
            return null;
        }
    }
}
